package com.griddroid.nodes;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import android.os.Handler;
import android.os.Message;
import com.griddroid.configuration.GridClientConfig;
import com.griddroid.configuration.GridConfig;
import com.griddroid.messages.EndSessionAckMessage;
import com.griddroid.messages.EndSessionRequestMessage;
import com.griddroid.messages.PingAckMessage;
import com.griddroid.messages.PingMessage;
import com.griddroid.messages.StartSessionAckMessage;
import com.griddroid.messages.StartSessionRequestMessage;

/**
 * This class represents a GridNode.Its an abstract class and implements the main functionality of a grid Node.
 * It includes a Socket {@link Socket Socket}to communicate with the server node,
 * an enumeration with the available grid node types (Server,Client,Worker)and
 * an check counter counting the number of checks before sending a ping message. Finally it also includes a message 
 * handler to communicate with the activity running the node and a boolean forceExit which when set true the node exits
 * 
 * @author Charalampos Mavroforakis
 * @author Thomas Ailianos
 * @author Michail Michailidis
 * 
 */
public abstract class GridNode
{
	/**
	 * Enumeration to characterize all grid nodes
	 */
	public static enum NodeType
	{
		SERVER, CLIENT, WORKER
	};

	/**
	 * Node type to print the right name in the log.
	 */
	protected NodeType type;

	/**
	 * Socket to communicate with the server. If the node is server then the
	 * variable is null
	 */
	protected Socket handler;
	/**
	 * Number of application checks before sending ping message.
	 */
	
	protected int checkCounter;
	
	/**
	 * Is set true to enforce exit of the node.
	 */
	protected boolean forceExit = false;
	
	/**
	 * Message handler to communicate with the activity.
	 */
	protected Handler messageHandler;
	private String TAG = "-";
	private String id;

	public GridNode()
	{
	}

	/**
	 * Writes a message into the log.
	 * 
	 * @param header
	 *            Header of the info.
	 * @param info
	 *            Info details
	 */
	public void log(String header, String info)
	{
		if (messageHandler != null)
		{
			String out = this.type + TAG + header + TAG + info;
			Message m = Message.obtain();
			m.what = GridConfig.UPDATE_TEXTVIEW;
			m.obj = out;
			this.messageHandler.sendMessage(m);
			System.out.println(out);
		}
		else
		{
			System.out.println(this.type + TAG + "HaNdLeR == NuLL");
			System.out.println(this.type + TAG + header + TAG + info);
		}
	}

	/**
	 * Sets the handler in order to communicate with the activity.
	 * 
	 * @param a
	 *            Handler to communicate with the activity running the node
	 */
	public void setHandler(Handler a)
	{
		if (a == null)
		{
			System.out.println("DO NOT GIVE MY NUUUUUUUUUUUUUUUULL!!!!");
		}
		this.messageHandler = a;
	}

	/**
	 * Returns the handler to communicate with the activity running the grid
	 * node
	 */
	public Handler getHandler()
	{
		return this.messageHandler;
	}

	/**
	 * Connects the node with the grid server node. Firstly a socket is opening,
	 * a {@link StartSessionRequestMessage StartSessionRequestMessage} is sent.
	 * Then the node waits until a {@link StartSessionAckMessage
	 * StartSessionAckMessage} is received containing info about the permanent
	 * connection with the right handler. The Three-Way Handshake is completed
	 * with the connection establishment with the right type of handler.
	 * 
	 */
	protected void connectToServer() throws UnknownHostException, IOException
	{
		// SEND CONNNECTION REQUEST
		Socket socket = new Socket(InetAddress.getByName(GridClientConfig.SERVERIP), GridClientConfig.SERVERPORT);
		StartSessionRequestMessage message = new StartSessionRequestMessage(this.type);
		this.send(socket, message);

		// GET ACKNOWLEDGMENT MESSAGE
		Object messG = null;
		try
		{
			messG = this.receive(socket);
		}
		catch (IOException e)
		{
			this.log("Error", "Could not receive startsessionAckMessage.");
			System.exit(0);
		}
		catch (ClassNotFoundException e)
		{
			this.log("Error", "Could not receive startsessionAckMessage.");
			System.exit(0);
		}

		if (messG == null || !(messG instanceof StartSessionAckMessage))
		{
			this.log("Error", "Could not receive startsessionAckMessage.");
			System.exit(0);
		}
		StartSessionAckMessage startSessionAckmessage = (StartSessionAckMessage) messG;
		this.log("Info", "Got StartSessionACKMessage");
		System.out.println("1");

		// START THE PERMANENT CONNECTION
		handler = null;
		try
		{
			handler = new Socket(InetAddress.getByName(GridClientConfig.SERVERIP), startSessionAckmessage.getHandlerPort());
		}
		catch (UnknownHostException e)
		{
			this.log("Error", "Could not establish main connetion to server.");
			System.exit(0);
		}
		catch (IOException e)
		{
			this.log("Error", "Could not establish main connetion to server.");
			System.exit(0);
		}

		this.setId(startSessionAckmessage.getId());
		this.log("Info", "Established permanent connection with the server");

	}

	/**
	 * Function to start the node. It must be overridden from the nodes.
	 */
	public abstract void start();

	/**
	 * Function to close the node. It must be overridden from the nodes.
	 */
	public abstract void close();

	/**
	 * Checks if a timeout is reached. If yes the close function is called.
	 */
	protected void checkForTimeout()
	{
		if (this.checkCounter * GridConfig.CHECKINTERVAL / 1000 > GridConfig.ALIVETIMEOUT)
			this.close();
	}

	/**
	 * Function responsible to receive a {@link PingMessage PingMessage} and
	 * send a {@link PingAckMessage PingAckMessage}
	 * 
	 * @param temp
	 *            Object which is instance of {@link PingMessage PingMessage}
	 */
	protected void getPingMessage(Object temp)
	{
		this.log("Info", "Got ping message");
		if (this.getId().equals(((PingMessage) temp).getNodeId()))
		{
			PingAckMessage pingAck = new PingAckMessage(this.getId());
			try
			{
				this.send(this.handler, pingAck);
				this.checkCounter = 0;
				this.log("Info", "Sent ping  ack smessage");
			}
			catch (IOException e)
			{
				this.log("Error", "Error sending ping acknowledge!" + e.getMessage());
			}
		}
		else
		{
			this.log("Error", "Got foregn ping message");
		}
	}

	/**
	 * Sends a {@link EndSessionRequestMessage EndSessionRequestMessage} to the
	 * handler. If this cannot be sent (meaning that the server is down)then the
	 * node exits
	 */
	public void sendEndSessionMessage()
	{
		try
		{
			this.send(this.handler, new EndSessionRequestMessage(this.id));
		}
		catch (IOException e)
		{
			this.log("Error", "Could not send end session message");
			this.close();
		}
	}

	/**
	 * Receives an {@link EndSessionAckMessage EndSessionAckMessage} and if its
	 * right the node exits
	 * 
	 * @param temp
	 *            Object that is instance of EndSessionAckMessage which was just
	 *            received
	 */
	protected void getEndAckSessionMessage(Object temp)
	{
		if (((EndSessionAckMessage) temp).getId().equals(this.getId()))
		{
			this.log("Info", "Received end session ack message from client handler with id " + this.getId());
			this.close();
		}
		else
		{
			this.log("Error", "Client with id " + this.getId() + " received unknown session end ack message");
		}
	}

	/**
	 * Function responsible to receive a message from the handler.
	 * 
	 * @param socket
	 *            The socket from which is supposed to receive a message.
	 * @return an object which under normal circumstances will be instance of
	 *         {@link GenericMessage GenericMessage}
	 * 
	 */
	protected Object receive(Socket socket) throws IOException, ClassNotFoundException
	{
		return (new ObjectInputStream(socket.getInputStream())).readObject();
	}

	/**
	 * Function responsible to send a message though the given socket.
	 * 
	 * @param socket
	 *            The socket though which it will send the Object (mostly an
	 *            instance of GenericMessage)
	 * 
	 */
	protected void send(Socket socket, Object o) throws IOException
	{
		(new ObjectOutputStream(socket.getOutputStream())).writeObject(o);
	}

	/**
	 * Returns the node id.
	 * 
	 * @return Returns the node id
	 */
	public String getId()
	{
		return id;
	}

	/**
	 * Sets the node id.
	 * 
	 * @param id
	 *            The node will take the given id.
	 */
	public void setId(String id)
	{
		this.id = id;
	}
}
